package ee.parameterConverter.impl;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import ee.metadataUtils.ObjectFactory;
import ee.metadataUtils.PropertyMetadataRegistry;
import ee.metadataUtils.impl.AdapterNotFoundException;
import ee.parameterConverter.ComplexConverter;
import ee.parameterConverter.ConversionFailedException;
import ee.parameterConverter.Converter;
import ee.parameterConverter.ConverterResolver;
import ee.parameterConverter.EmptyValueException;
import ee.parameterConverter.ParameterPropertyMetadata;
import ee.parameterConverter.ValidationResultCollector;
import ee.parameterConverter.Validator;

public class ComplexConverterImpl implements ComplexConverter {

	private ParameterConverterImpl _parameterConverter;
	private ConverterResolver _converterResolver;
	
	private PropertyMetadataRegistry _metadataRegistry;
	private ObjectFactory _objectFactory;
	private ValidationResultCollector _validationResultCollector;
	
	public ComplexConverterImpl(ParameterConverterImpl parameterConverter, ConverterResolver converterResolver, ValidationResultCollector validationResultCollector, ObjectFactory objectFactory, PropertyMetadataRegistry metadataRegistry)
	{
		_parameterConverter = parameterConverter;
		_converterResolver = converterResolver;
		_metadataRegistry = metadataRegistry;
		_objectFactory = objectFactory;
		_validationResultCollector = validationResultCollector;
	}
	
	@Override
	public Object convert(Map<String, String[]> parameters, Type type) throws EmptyValueException, ConversionFailedException {
		return convert(parameters, type, null);
	}

	@Override
	public Object convert(Map<String, String[]> parameters, Type type, String parameterKey) throws EmptyValueException, ConversionFailedException {
		
		// see if we have a converter
		Converter<Map<String, String[]>, ?> converter = _converterResolver.getParameterConverter(type);

		if (converter != null) {
			return converter.convert(parameters);
		}
		
		Map<String, Object> normalizedParameters = _parameterConverter.normalizeParameters(parameters, parameterKey);
		ArrayList<String> parameterPath = new ArrayList<String>();
		
		if (parameterKey != null)
		{
			parameterPath.add(parameterKey);
			if (normalizedParameters.containsKey(parameterKey))
			{
				Object result = normalizedParameters.get(parameterKey);
				
				if (result instanceof Map) {
					@SuppressWarnings("unchecked")
					Map<String, Object> castResult = (Map<String, Object>) result;
					normalizedParameters = castResult;
				} else if (result instanceof String[])
				{
					//we can not convert this, let the parameter converter handle it
					return _parameterConverter.convert((String[]) result, type);
				} else
				{
					throw new ConversionFailedException("Could not convert given parameters with key '" + parameterKey + "' to type " + type);
				}
			} else
			{
				throw new EmptyValueException("Could not find key '" + parameterKey + "' in normalized parameters");
			}
		}
		
		return convert(normalizedParameters, type, parameterPath);
	}

	@Override
	public Object convert(Map<String, Object> normalizedParameters, Type type,
			List<String> parameterPath) throws ConversionFailedException, EmptyValueException {
		
		Object object;
		try {
			object = _objectFactory.create(type);
		} catch (InstantiationException e) {
			throw new ConversionFailedException("Could not create instance of type " + type, e);
		}

		Type rawType = type;
		
		if (type instanceof ParameterizedType)
		{
			rawType = ((ParameterizedType) type).getRawType();
		}
		
		if (!(rawType instanceof Class))
		{
			throw new ConversionFailedException("Can only convert to types that are instances of Class or are ParameterizedTypes that have a raw type that is an instance of Class");
		}
		
		Collection<ParameterPropertyMetadata> metadata;
		try {
			metadata = _metadataRegistry.getPropertyMetadataCollection((Class<?>) rawType, ParameterPropertyMetadata.class);
		} catch (AdapterNotFoundException e) {
			throw new ConversionFailedException("Could not convert value, adapter for ParameterPropertyMetadata was probably not registered", e);
		}

		Set<ExceptionHolder> exceptions = new HashSet<ExceptionHolder>();
		int size = metadata.size();
		int empty = 0;
		
		for (ParameterPropertyMetadata propertyMetadata : metadata) {
			String name = propertyMetadata.getName();

			ArrayList<String> currentParameterPath = new ArrayList<String>(parameterPath);
			currentParameterPath.add(name);

			try {
				try {
					if (normalizedParameters.containsKey(name)) {
						Object originalValue = normalizedParameters.get(name);

						Object value;
						Type propertyType = propertyMetadata.getGenericType();

						Converter<String[], ?> customConverter = propertyMetadata.getConverter();
						
						if (customConverter != null) {
							value = customConverter.convert((String[]) originalValue);
						} else if (originalValue instanceof Map) {
							@SuppressWarnings("unchecked")
							Map<String, Object> originalValueMap = (Map<String, Object>) originalValue;

							value = _parameterConverter.convert(originalValueMap, propertyType,
									currentParameterPath);
						} else {
							// convert the value
							value = _parameterConverter.convert((String[]) originalValue, propertyType);
						}

						Validator validator = propertyMetadata.getValidator();

						if (validator != null) {
							validator.validate(value);
						}

						propertyMetadata.setValue(object, value);
					} else {
						throw new EmptyValueException("The value for property '" + name
								+ "' could not be found");
					}
				} catch (EmptyValueException e) {
					empty++;
					if (propertyMetadata.isOptional()) {
						// no problem, just use the default value
						propertyMetadata.setValue(object, propertyMetadata.getDefaultValue(object));

					} else {
						throw e;
					}
				}
			} catch (Throwable e) {
				exceptions.add(new ExceptionHolder(currentParameterPath, e));
			}
		}

		if (empty == size) {
			throw new EmptyValueException("All properties threw an empty value exception");
		} else {
			for (ExceptionHolder exceptionHolder : exceptions) {
				_validationResultCollector.processException(exceptionHolder.currentParameterPath, exceptionHolder.exception);
			}
		}
		
		return object;
	}
	
	class ExceptionHolder
	{
		List<String> currentParameterPath;
		Throwable exception;

		ExceptionHolder(List<String> currentParameterPath, Throwable exception)
		{
			this.currentParameterPath = currentParameterPath;
			this.exception = exception;
		}
	}

}
